<!DOCTYPE HTML>
<html>
<script src="test.js"></script>
<script src="call_function.js"></script>
<script>
function wrapArgs(value, w3c) {
  if (typeof w3c === "undefined") {
    w3c = true;
  }
  const nodes = [];
  const preprocessed = preprocessResult(value, [], nodes);
  const stringified = JSON.stringify(preprocessed);
  return [JSON.parse(stringified), w3c, nodes];
}

function unwrapResult(value) {
  return resolveReferences(JSON.parse(value[0]), value.slice(1));
}

function serialize(value) {
  const nodes = [];
  const preprocessed = preprocessResult(value, [], nodes);
  return [JSON.stringify(preprocessed), ...nodes];
}

function deserialize(serialized) {
  return resolveReferences(JSON.parse(serialized[0]), serialized.slice(1));
}

function roundtrip(value) {
  return deserialize(serialize(value));
}

function testSerialize() {
  assertEquals(1, roundtrip(1));
  assertEquals("1", roundtrip("1"));
  assertEquals("chromium", roundtrip("chromium"));
  assertEquals(false, roundtrip(false));
  assertEquals(null, roundtrip(null));
  assertEquals(undefined, roundtrip(undefined));
  assertEquals(JSON.stringify([]), JSON.stringify(roundtrip([])));
  assertEquals(JSON.stringify(["uno", 2]), JSON.stringify(roundtrip(["uno", 2])));
  assertEquals(JSON.stringify({}), JSON.stringify(roundtrip({})));
  assertEquals(JSON.stringify({}),
               JSON.stringify(roundtrip(function(){console.log("Hello, World!");})));

  const div = document.querySelector("div");
  assertEquals(div, roundtrip(div));
  assertEquals(document, roundtrip(document));

  const divs = document.querySelectorAll("div");
  const deserialized = roundtrip(divs);
  assertEquals(divs[0], deserialized[0]);
  assertEquals(divs[1], deserialized[1]);
}

function testDeepSerialization() {
  const original = [1, new Array(1, new Object({a: 1, b: {a: 1, b: {}, c: 3}}), 3)];
  const originalJson = JSON.stringify(original);

  original[1][1].b.b = document;
  let deserialized = roundtrip(original);
  assertEquals(document, deserialized[1][1].b.b);

  const div = document.querySelector("div");
  original[1][1].b.b = div;
  deserialized = roundtrip(original);
  assertEquals(div, deserialized[1][1].b.b);

  deserialized[1][1].b.b = {};
  assertEquals(originalJson, JSON.stringify(deserialized));
}

function testObjectWithLengthProperty() {
  let obj = {length: 200};
  assertEquals(200, roundtrip({length: 200})["length"])
  assertEquals(JSON.stringify(obj), JSON.stringify(roundtrip({length: 200})))

  obj = {bar: "foo", bazz: {length: 150}};
  const deserialized = roundtrip(obj);
  assertEquals("foo", deserialized["bar"])
  assertEquals(150, deserialized["bazz"]["length"])
  assertEquals(JSON.stringify(obj), JSON.stringify(deserialized));
}

function testDeserializeElementReference() {
  const original = {};
  original[ELEMENT_KEY] = 0;
  const json = JSON.stringify(original);
  deserialized = deserialize([json, document]);
  assertEquals(deserialized, document);
}

function testDeserializeNegativeIndexThrows() {
  const original = {};
  original[ELEMENT_KEY] = -1;
  const json = JSON.stringify(original);
  let thrown = false;
  try {
    deserialize([json, document]);
  } catch (e) {
    thrown = true;
  }
  assert(thrown);
}

function testDeserializeIndexOutOfRange() {
  const original = {};
  original[ELEMENT_KEY] = 1;
  const json = JSON.stringify(original);
  let thrown = false;
  try {
    deserialize([json, document]);
  } catch (e) {
    thrown = true;
  }
  assert(thrown);
}

function testStaleRef() {
  const img = document.createElement("img");
  document.body.appendChild(img);
  const serialized = serialize(img);
  document.body.removeChild(img);
  let thrown = false;
  try {
    deserialize(serialized);
  } catch (e) {
    thrown = true;
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, e.code);
  }
  assert(thrown);
}

function testDeserializeWithNodeIndexOutOfRange() {
  const img = document.createElement("img");
  document.body.appendChild(img);
  const serialized = [serialize(img)[0]];
  let thrown = false;
  try {
    deserialize(serialized);
  } catch (e) {
    thrown = true;
    assertEquals(StatusCode.JAVA_SCRIPT_ERROR, e.code);
  }
  assert(thrown);
}

function testSerializationWithShadowDomAttached() {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  // Test with attached element in shadow DOM.
  const deserialized = roundtrip(shadowDiv);
  assertEquals(shadowDiv, deserialized);

  document.body.removeChild(host);
}

function testSerializeDetachedElementInShadowTree() {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  // Test with detached element in shadow DOM.
  root.removeChild(shadowDiv);
  let thrown = false;
  try {
    serialize(shadowDiv);
    document.body.removeChild(host);
  } catch (e) {
    thrown = true;
    document.body.removeChild(host);
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, e.code);
  }

  assert(thrown)
}

function testDeserializeDetachedElementInShadowTree() {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  // Test with detached element in shadow DOM.
  const serialized = serialize(shadowDiv);
  root.removeChild(shadowDiv);
  let thrown = false;
  try {
    deserialize(serialized);
    document.body.removeChild(host);
  } catch (e) {
    thrown = true;
    document.body.removeChild(host);
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, e.code);
  }

  assert(thrown)
}

function testSerializeElementInDetachedShadowTree() {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  // Test with detached element in shadow DOM.
  document.body.removeChild(host);
  let thrown = false;
  try {
    serialize(shadowDiv);
  } catch (e) {
    thrown = true;
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, e.code);
  }

  assert(thrown)
}

function testDeserializeElementInDetachedShadowTree() {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  // Test with detached element in shadow DOM.
  const serialized = serialize(shadowDiv);
  document.body.removeChild(host);
  let thrown = false;
  try {
    deserialize(serialized);
  } catch (e) {
    thrown = true;
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, e.code);
  }

  assert(thrown)
}

function testSerializeDetachedShadowTreeRoot() {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  // Test with detached element in shadow DOM.
  document.body.removeChild(host);
  let thrown = false;
  try {
    serialize(root);
  } catch (e) {
    thrown = true;
    assertEquals(StatusCode.DETACHED_SHADOW_ROOT, e.code);
  }

  assert(thrown)
}

function testDeserializeDetachedShadowTreeRoot() {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  // Test with detached element in shadow DOM.
  const serialized = serialize(root);
  document.body.removeChild(host);
  let thrown = false;
  try {
    deserialize(serialized);
  } catch (e) {
    thrown = true;
    assertEquals(StatusCode.DETACHED_SHADOW_ROOT, e.code);
  }

  assert(thrown)
}

// Verify array serialization works when Array.prototype.toJSON is defined.
function testSerializeArrayToJsonProto(runner) {
  Array.prototype.toJSON = () => "proto";
  const original = [1, 7, "tres"];
  const result = roundtrip(original);
  delete Array.prototype.toJSON;
  assert(result instanceof Array);
  assertEquals(result[0], 1);
  assertEquals(result[1], 7);
  assertEquals(result[2], "tres");
  assertEquals(JSON.stringify(original), JSON.stringify(result));
}

function testSerializeArrayToJsonOwn(runner) {
  const original = [10, 7, "tres"];
  original.toJSON = () => "own";
  const result = roundtrip(original);
  assertEquals(result, "own");
  assertEquals(JSON.stringify(original), JSON.stringify(result));
}

function testSerializeArrayToJsonOwnAndProto(runner) {
  Array.prototype.toJSON = () => "proto";
  const original = [10, 33, "tres"];
  original.toJSON = () => "own";
  const result = roundtrip(original);
  delete Array.prototype.toJSON;
  assertEquals(result, "own");
  assertEquals(JSON.stringify(original), JSON.stringify(result));
}

function testDeepSerializationCustomToJson() {
  const fancy = [4, 16, 64];
  fancy.toJSON = () => "fancy-tojson";
  const original = [1, Array(1, new Object({a: [3, 27], b: {a: 1, b: {}, c: fancy}}), 3)];
  const originalJson = JSON.stringify(original);
  const div = document.querySelector("div");
  original[1][1].b.b = div;

  Array.prototype.toJSON = () => "proto";
  deserialized = roundtrip(original);
  delete Array.prototype.toJSON;

  assertEquals(div, deserialized[1][1].b.b);
  assertEquals("fancy-tojson", deserialized[1][1].b.c);
  deserialized[1][1].b.b = {};
  assertEquals(originalJson, JSON.stringify(deserialized));
}

function testDeepSerializationCustomToJsonNestedArrays() {
  const fancy = [4, [5, 55], 64];
  fancy.toJSON = () => "fancy-tojson";
  const original = [111, fancy, "third"];
  const originalJson = JSON.stringify(original);

  Array.prototype.toJSON = () => "proto";
  deserialized = roundtrip(original);
  delete Array.prototype.toJSON;

  assertEquals(111, deserialized[0]);
  assertEquals("fancy-tojson", deserialized[1]);
  assertEquals("third", deserialized[2]);
  assertEquals(originalJson, JSON.stringify(deserialized));
}

function testCallFunctionNoArgs(runner) {
  callFunction(function() { return 1; }, [], true, []).then((resultArray) => {
    const value = unwrapResult(resultArray).value;
    assertEquals(1, value);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallFunctionThrows(runner) {
  let allComplete = 0;
  callFunction(function() { throw new Error("fake error"); },
    [], true, []).then((result) => {
      const unwrapped = unwrapResult(result);
      assertEquals(StatusCode.JAVA_SCRIPT_ERROR, unwrapped.status);
      assertEquals("fake error", unwrapped.value);
      if (allComplete)
        runner.continueTesting();
      allComplete += 1;
    });
  callFunction(function() {
      const e = new Error("fake error");
      e.code = 77;
      e.message = "CUSTOM";
      throw e;
  }, [], true, []).then((result) => {
    const unwrapped = unwrapResult(result);
    // Scripts used by ChromeDriver can also return valid codes.
    // Example: Script CLEAR in Selenium Atoms.
    // Therefore all codes are propagated intact.
    assertEquals(77, unwrapped.status);
    assertEquals("CUSTOM", unwrapped.value);
    if (allComplete)
      runner.continueTesting();
    allComplete += 1;
  });
  runner.waitForAsync();
}

function testCallWithMalformedIdThrows() {
  const original = {};
  original[ELEMENT_KEY] = "malformed_id";
  callFunction(function(){}, ...wrapArgs([original])).then((resultArray) => {
    const unwrapped = unwrapResult(result);
    assertEquals(StatusCode.STALE_ELEMENT_REFERENCE, unwrapped.status);
    assertEquals("stale element not found", unwrapped.value);
  });
}

function testCallFunctionArgs(runner) {
  function func(primitive, elem) {
    return [primitive, elem.querySelector("div")];
  }
  callFunction(func, ...wrapArgs([1, document])).then((resultArray) => {
    const values = unwrapResult(resultArray).value;
    assertEquals(1, values[0]);
    assertEquals(document.querySelector("div"), values[1]);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallFunctionArgsNonW3C(runner) {
  function func(elem) {
    return elem.querySelector("div");
  }
  callFunction(func, ...wrapArgs([document], w3c=false)).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assertEquals(document.querySelector("div"), result);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallFunctionWithShadowHost(runner) {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  function func(element) {
    return element;
  }
  callFunction(func, ...wrapArgs([host])).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assertEquals(host, result);
    document.body.removeChild(host);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallFunctionWithShadowRoot(runner) {
  // Set up something in the shadow DOM.
  const host = document.body.appendChild(document.createElement("div"));
  const root = host.attachShadow({ mode: "open" });
  const shadowDiv = root.appendChild(document.createElement("div"));

  function func(element) {
    return element;
  }
  // Should handle shadow root as an argument.
  callFunction(func, ...wrapArgs([root])).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assertEquals(root, result);
    document.body.removeChild(host);
    runner.continueTesting();
  });
  runner.waitForAsync();
}

// Verify callFunction works when Object.prototype has user-defined functions.
// (https://crbug.com/chromedriver/3074)
function testCallWithFunctionInObject(runner) {
  Object.prototype.f = () => {};
  function func(arg) {
    return { bar: arg };
  }
  callFunction(func, ...wrapArgs([{ foo: 1 }])).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assertEquals(1, result.bar.foo);
    delete Object.prototype.f;
    runner.continueTesting();
  });
  runner.waitForAsync();
}

// Verify array serialization works when Array.prototype.toJSON is defined.
// (https://crbug.com/chromedriver/3084)
function testCallWithArrayToJsonProto(runner) {
  Array.prototype.toJSON = () => "[\"testing\"]";
  function func() {
    return [111, "dos", 3];
  }
  callFunction(func, ...wrapArgs([])).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assert(result instanceof Array);
    assertEquals(result.length, 3);
    assertEquals(result[0], 111);
    assertEquals(result[1], 'dos');
    assertEquals(result[2], 3);
    delete Array.prototype.toJSON;
    runner.continueTesting();
  });
  runner.waitForAsync();
}

// Verify array serialization works when own property toJSON is defined.
// (https://crbug.com/chromedriver/4325)
function testCallWithArrayToJsonOwn(runner) {
  function func() {
    const result = [1, 2, 3];
    result.toJSON = () => "[\"testing\"]";
    return result;
  }
  callFunction(func, ...wrapArgs([])).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assertEquals(typeof result, "string");
    assertEquals(result, "[\"testing\"]");
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallWithArrayToJsonOwnAndProto(runner) {
  Array.prototype.toJSON = () => "proto";
  function func() {
    const result = [1, 2, 3];
    result.toJSON = () => "own";
    return result;
  }
  callFunction(func, ...wrapArgs([])).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assertEquals(typeof result, "string");
    assertEquals(result, "own");
    delete Array.prototype.toJSON;
    runner.continueTesting();
  });
  runner.waitForAsync();
}

function testCallWithModifiedObjectProto(runner) {
  let callCount = 0;
  Object.defineProperty(Object.prototype, "f", {
    enumerable: true,
    configurable: true,
    get: function() {
      callCount += 1;
    }
  });

  callFunction(function() {}, []).then(() => {
    try {
      assertEquals(callCount, 0);
      runner.continueTesting();
    } finally {
      delete Object.prototype.f;
    }
  });
  runner.waitForAsync();
}

function testCallFunctionWithLargeData(runner) {
  const original = "0".repeat(10e6);
  function func(arg) {
    return arg;
  }
  callFunction(func, ...wrapArgs([original])).then((resultArray) => {
    const result = unwrapResult(resultArray).value;
    assertEquals(result, original);
    runner.continueTesting();
  });
  runner.waitForAsync();
}
</script>
<body>
<div><span>div1</span></div>
<div><span>div2</span></div>
</body>
</html>
